package com.ajavaer.framework.core.orm.dao.impl;

import com.ajavaer.framework.common.spring.SpringContext;
import com.ajavaer.framework.config.AjavaerConfig;
import com.ajavaer.framework.core.orm.RunSQL;
import com.ajavaer.framework.core.orm.TemplateBuilder;
import com.ajavaer.framework.core.orm.dao.BaseDAO;
import com.ajavaer.framework.core.tools.FreemarkerTools;
import com.ajavaer.framework.exception.SQLTemplateException;
import freemarker.template.Configuration;
import freemarker.template.Template;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.Resource;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;

import javax.persistence.EntityManager;
import javax.persistence.metamodel.EntityType;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class BaseDAOImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, Serializable> implements BaseDAO<T, Serializable>, InitializingBean {
    private Logger log = LoggerFactory.getLogger(BaseDAOImpl.class);
    private final EntityManager entityManager;
    private Class<T> domainClass;
    private Map<Class<?>, Resource> sqlResources = new ConcurrentHashMap<>();
    private Map<Class<?>, Long> lastModified = new ConcurrentHashMap<>();
    private Map<Class<?>, Map<String, Template>> templateIdMap = new ConcurrentHashMap<>();
    private Configuration configuration;
    private RunSQL runSQL;
    private AjavaerConfig ajavaerConfig;

    public BaseDAOImpl(Class<T> domainClass, EntityManager em) {
        super(domainClass, em);
        this.entityManager = em;
        this.domainClass = domainClass;
    }

    public BaseDAOImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager em) {
        super(entityInformation, em);
        this.entityManager = em;
        this.domainClass = entityInformation.getJavaType();
    }

    @Override
    public T getById(Serializable id) {
        return getOne(id);
    }

    @Override
    public List<T> findByIdIn(Collection ids) {
        return findAllById(ids);
    }

    @Override
    public int templateUpdate(String templateId, Map params) {
        return runSQL.executeUpdate(templateSql(templateId, params), params);
    }

    @Override
    public int templateUpdate(EntityManager entityManager, String templateId, Map params) {
        return runSQL.executeUpdateWithoutTx(entityManager, templateSql(templateId, params), params);
    }

    @Override
    public List<Map<String, Object>> templateQuery(String templateId, Map params) {
        return runSQL.executeMap(templateSql(templateId, params), params);
    }

    @Override
    public List<T> templateQuery(String templateId, Map params, Class<T> clazz) {
        return runSQL.executeObject(templateSql(templateId, params), clazz, params);
    }

    @Override
    public TemplateBuilder templateBuilder(String templateId) {
        return new TemplateBuilder(getTemplate(templateId));
    }

    private String templateSql(String templateId, Map params) {
        Template template = getTemplate(templateId);
        if (template == null) {
            throw new SQLTemplateException("template " + parseTemplateId(templateId) + " not found");
        }
        return FreemarkerTools.processTemplate(template, params).trim();
    }

    private Template getTemplate(String templateId) {
        if (this.runSQL == null) {
            initSqlTemplate();
        }
        if (ajavaerConfig.getDebug()) {//debug模式时,如果模版变化,自动重载模版
            reloadIfPossible();
        }
        Map<String, Template> templateMap = templateIdMap.get(domainClass);
        return templateMap.get(parseTemplateId(templateId));
    }

    private String parseTemplateId(String id) {
        return domainClass.getName() + "." + id;
    }

    private void initSqlTemplate() {
        this.runSQL = SpringContext.getBean(RunSQL.class);
        this.ajavaerConfig = SpringContext.getBean(AjavaerConfig.class);
        this.configuration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
        try {
            Set<EntityType<?>> entities = entityManager.getMetamodel().getEntities();
            for (EntityType<?> entity : entities) {
                String templateLocation = "classpath*:sqls/";
                String suffixPattern = ".sftl";
                Resource resource = SpringContext.getResource(templateLocation + entity.getJavaType().getTypeName().replaceAll("\\.", "/") + suffixPattern);
                if (resource != null && resource.exists()) {
                    sqlResources.put(entity.getJavaType(), resource);
                    lastModified.put(entity.getJavaType(), resource.lastModified());
                    loadTemplate(entity.getJavaType(), resource);
                }
            }
        } catch (IOException e) {
            log.error("read sql template error:" + e.getMessage(), e);
            System.exit(-1);
        }
    }

    private void loadTemplate(Class<?> clazz, Resource resource) throws IOException {
        if (resource != null && resource.exists()) {
            try (BufferedReader bufferedReader = new BufferedReader(new FileReader(resource.getFile()))) {
                String line;
                String templateId = null;
                StringBuilder stringBuilder = new StringBuilder();
                while ((line = bufferedReader.readLine()) != null) {
                    line = line.trim();
                    //get template id
                    if (line.startsWith("##")) {
                        if (stringBuilder.length() > 0) {
                            putTemplate(templateId, stringBuilder, clazz);
                            stringBuilder.delete(0, stringBuilder.length() - 1);
                        }
                        templateId = clazz.getTypeName() + "." + line.replaceAll("##", "").split("//")[0].trim();
                    } else {
                        //read template
                        stringBuilder.append(line).append("\n");
                    }
                }
                if (stringBuilder.length() > 0) {
                    putTemplate(templateId, stringBuilder, clazz);
                }
            }
        }
    }

    private void putTemplate(String templateId, StringBuilder stringBuilder, Class clazz) throws IOException {
        //put template
        Template template = new Template(templateId, stringBuilder.toString(), configuration);
        Map<String, Template> templateMap = templateIdMap.get(clazz);
        if (templateMap == null) {
            templateMap = new ConcurrentHashMap<>();
            templateIdMap.put(clazz, templateMap);
        }
        templateMap.put(templateId, template);
    }


    private void reloadIfPossible() {
        try {
            Long lastModified = this.lastModified.get(domainClass);
            Resource resource = sqlResources.get(domainClass);
            long newLastModified = resource.lastModified();
            //check modified for cache.
            if (lastModified == null || newLastModified > lastModified) {
                this.lastModified.put(domainClass, newLastModified);
                //process template.
                loadTemplate(domainClass, resource);
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        if (this.runSQL == null) {
            initSqlTemplate();
        }
    }
}
